<?php
namespace DRY\Module;

use DRY\core;
use DRY\Water;

/**
 * DonReY Framework 2012 :: Application organizer and "Controller" manager in an MVC pattern
 * - controller loading / handling
 * - URI Analyser/filter + Code Router
 * - Input (GET/POST) grabber/filter *
 * Note: This module should only exist in one instance!
 *
 * @author Wavetrex <wavetrex@gmail.com>
 * @link http://www.donrey.net/Documentation/Structure/Modules/application.html
 * @license [GPL v3] http://www.gnu.org/licenses/gpl-3.0.html
 * @version 1.0.dev
 * @package DonReY
 * @subpackage Module.Application
 */
class Application extends base
{
	// Exception/Error codes
	const
		EXC_PATHS_NOTSET = 1,
		EXC_CTRLPATH_MISSING = 2,
		EXC_CTRL_NOTALLOWED = 3,
		EXC_CTRL_CLASSNOTFOUND = 4,
		EXC_NOROUTE_HANDLERS = 5,
		EXC_ROUTE_NOTFOUND = 6,
		EXC_ROUTE_NOTASSIGNED_TO_CTRL = 7,
		EXC_CTRL_FORWARD_LOOP_TRAP = 8,
		EXC_CTRL_INSIDE_THROW = 9
		;

	/**
	 * Handler for commands from the Control module
	 * @param string $command
	 */
	public function control_Command($command)
	{
		switch($command) {
			case 'ExportConfig':
				$exp = array('module'=> array('alias'=> $this->_alias));
				$exp['paths'] = core::assemble_Absolute_Paths($this-> CFG-> paths); // APP_PATH is defaut

				// constructs an array of all controllers and the routes they handle
				$exp['controllers']=array();
				foreach($this-> CFG-> controllers-> children() as $controller_name=>$controller)
				{
					$ctrl_routes = array();
					if(isset($controller['handles'])) {
						$handles = explode(',', $controller['handles']);
						foreach($handles as $route_name)
							$ctrl_routes[] = array(
								'route'=> $route_name,
								'regex'=> (string)$this-> CFG-> routes-> {$route_name},
								'expand'=> json_decode((string)$this-> CFG-> routes-> {$route_name}['expand'], true),
								'assign'=> json_decode((string)$this-> CFG-> routes-> {$route_name}['assign'], true)
							);
					}
					$exp['controllers'][] = array(
						'controller'=> $controller_name,
						'routes'=> $ctrl_routes
					);
				}

				return $exp;
		}
	}

//===== Controllers (Application components) =====//

	/**
	 * Module Constructor
	 * @param string $alias
	 * @param SimpleXMLElement $config
	 */
	public function __construct($alias, $config)
	{
		parent::__construct($alias, $config);
		// check required paths
		if(isset($this-> CFG-> paths)) {
			$paths = core::assemble_Absolute_Paths($this-> CFG-> paths); // APP_PATH is defaut

			// check if there is at least one valid controllers path and also register all paths with the core autoloader
			$found = false;
			foreach($paths['controllers'] as $controllers_path)
				if($controllers_path!==false) {
					core::register_Autoloader_Path('Controller_', $controllers_path.'/', '.controller.php', array($this, 'controller_Check'));
					$found = true;
				}
			if(!$found)
				throw new Water('application', self::EXC_CTRLPATH_MISSING);
		} else
			throw new Water('application', self::EXC_PATHS_NOTSET);

	    if(isset($this-> CFG-> routes)) {
	    	$this-> router_Init_from_CFG();
	    }
	}

	/**
	 * Callback used by the Core Autoloader to check if the controller is allowed to load
	 * @param string $controller_name
	 */
	public function controller_Check($controller_name)
	{
		if(isset($this ->CFG-> controllers) && !isset($this ->CFG ->controllers ->{$controller_name})) {
			Water::dump($controller_name, Water::read_Message('application', self::EXC_CTRL_NOTALLOWED), null, 'error');
			return false;
		}
		return true;
	}

//===== URI Analyser/Filter =====//

	/**
	 * Loads the existing URI string, until the ? character
	 * Optionally cuts a number of characters from the beginning, or performs any kind of operation on the URI using a callback function
	 *
	 * @param int $cut Number of characters to cut from the beginning of the URI ( defaults to 0 )
	 * @param callback $filter_callback Performs any kind of operation on the URI before passing it to the router
	 */
	public static function load_URI($cut=0, $filter_callback=null)
	{
		$uri = substr(strtok($_SERVER['REQUEST_URI'],'?'),$cut);
		if(is_callable($filter_callback))
			$uri = call_user_func($filter_callback,$uri);
		return $uri;
	}

//===== Router =====//

	/**
	 * List of all regex used to search for a match, in the format
	 * key = variant key
	 * value = array with:
	 * 		[regex] = regular expression
	 * 		[expand] = expand data (array), or null if data doesn't need to be expanded (optional)
	 * 		[assign] = assign to, to give names to regex entries, instead of numbers (optional)
	 *
	 * @var array
	 */
	protected $uri_variants=array();

	/**
	 * Adds an URI variant to the list, using the raw regex engine
	 *
	 * @param string $key Key for this variant, is returned when match is found
	 * @param string $regex Regular expression to check the URI against
	 * @param array $expand (optional) Which of the regex matches will get expanded into an array, in the format:
	 *   key = index of data match, starting from 1
	 *   value = character(s) used to split the data
	 * @param array $assign (optional) Which of the regex matches will also receive named key (instead of number), in the format:
	 *   key = index of data match, starting from 1
	 *   value = name of the array key
	 */
	public function router_Add_Regex($key, $regex, $expand=null, $assign=null)
	{
		if(isset($this->uri_variants[$key]))
			return; // error, variant already defined
		$this->uri_variants[$key]=array(
			'regex'=> $regex,
			'expand'=> $expand,
			'assign'=> $assign
		);
	}

	/**
	 * Resets the router paths (maybe for overriding config in some special cases ?)
	 */
	public function router_Reset()
	{
		$this->uri_variants=array();
	}

	/**
	 * Initializes the router paths from a config variable ( done automatically if that variable exists )
	 */
	private function router_Init_from_CFG()
	{
		foreach($this-> CFG-> routes-> children() as $route_name=>$uri_path) {
			// expand fragments?
            $expand = isset($uri_path['expand'])? json_decode((string)$uri_path['expand'], true):null;
            // assign as named key?
            $assign = isset($uri_path['assign'])? json_decode((string)$uri_path['assign'], true):null;
            // setup route
            $this-> router_Add_Regex($route_name,(string)$uri_path, $expand, $assign);
		}
	}

	/**
	 * Searches for the correct match for the passed URI. If none found, returns {false}
	 * NOTE: This function does not initiate the flow to the specified route, it only searches for it in the list of URIs. Use method Flow() for that purpose instead
	 * (which calls this function first to determine the the route, than creates the controller object and initializes it)
	 *
	 * @param string $uri Pass a custom URI to this function. If none passed, it loads the server passed URI using the built-in function load_URI()
	 *
	 * @return array|false
	 */
	public function router_Check($uri=null)
	{
		if(is_null($uri))
			$uri = self::load_URI();

		// decode the URI
		foreach($this->uri_variants as $key=>$definition) {

			if(preg_match('{'.$definition['regex'].'}i', $uri, $uri_data)) {
				// entry zero is the [key]
				$uri_data[0] = $key;
				// found, expand what's necessary
				if(is_array($definition['expand'])) {
					for($c=1; $c<=count($uri_data); $c++)
						if(isset($definition['expand'][$c])) {
							if(!empty($uri_data[$c]))
								$uri_data[$c] = explode($definition['expand'][$c], trim($uri_data[$c], $definition['expand'][$c]));
							else
								$uri_data[$c] = array();
						}
				}
				// assign as named key as well if 'assign' exists
				if(is_array($definition['assign'])) {
					for($c=1; $c<=count($uri_data); $c++)
						if(isset($definition['assign'][$c])) {
							$uri_data[$definition['assign'][$c]] = $uri_data[$c];
						}
				}
				return $uri_data;
			}
		}
		// if we got here, no variants found
		return false;
	}

	/**
	 * List of loaded application controllers
	 *
	 * @var array[DRY_Controller_base]
	 */
	private $controllers;

	/**
	 * Searches for an active route, discovers the controller associated with it, initializes the controller and runs it
	 *
	 * @param string $uri Pass a custom URI to this function. If none passed, it loads the server passed URI using the built-in function load_URI()
	 */
	public function Flow($uri = null)
	{
		if(!isset($this-> CFG-> controllers) || $this-> CFG-> controllers-> children()-> count() == 0)
			throw new Water('application', self::EXC_NOROUTE_HANDLERS);

		if(($found_route = $this-> router_Check($uri))===false)
			throw new Water('application', self::EXC_ROUTE_NOTFOUND);

		// find which controller handles the found route
		$route_names=array();
		foreach($this-> CFG-> controllers-> children() as $controller_name=>$controller)
		{
			if(isset($controller['handles'])) {
				$handles = explode(',', $controller['handles']);
				foreach($handles as $route_name)
					$route_names[$route_name] = $controller_name;
			}
		}

		// found a valid controller for the route
		if(isset($route_names[$found_route[0]])) {
			\DRY\Application\Controller_base::$_app_module = &$this;
			return $this-> controller_Loop($route_names[$found_route[0]], $found_route);
		} else
			throw new Water('application', self::EXC_ROUTE_NOTASSIGNED_TO_CTRL, array('found_route'=> $found_route[0]));
	}

	/**
	 * Executes the controller, handling forwards
	 * @param string $initial_controller
	 * @param array $uri
	 */
	public function controller_Loop($initial_controller, $route=array())
	{
		$secure=0;
		$cc = $initial_controller;
		do {
			$controller_class_name = 'Controller_'.ucfirst($cc);

			if(!isset($this-> controllers[$cc]))
				if(class_exists($controller_class_name, true))
					$this-> controllers[$cc] = new $controller_class_name($route); // creates the controller object if it doesn't yet exist
				else
					throw new Water('application', self::EXC_CTRL_CLASSNOTFOUND, array('controller'=>$cc));

			// catch any exceptions inside the controller and convert them to a visible error
			try {
				Water::dump($controller_class_name, "Application::Execute");
				$cc = $this-> controllers[$cc]-> Execute(); // runs it's execute function
			} catch(\Exception $e) {
				// controller bug.
				Water::caught_Exception($e, 'application', self::EXC_CTRL_INSIDE_THROW, array('controller'=> $cc));
				$cc = null;
			}

			// to prevent infinite loop, do not allow more than 10 forwards
			if(++$secure>10)
				throw new Water('application', self::EXC_CTRL_FORWARD_LOOP_TRAP);

		} while(is_string($cc));
		return $cc;
	}

	/**
	 * Path where views are kept in the application
	 * @var string
	 */
	private $view_path;

}

namespace DRY\Application;

/**
 * Base class for controllers
 *
 * @author Wavetrex <wavetrex@gmail.com>
 * @link http://www.donrey.net/Documentation/Structure/Modules/application.html
 * @license [GPL v3] http://www.gnu.org/licenses/gpl-3.0.html
 * @version 1.0.dev
 * @package DonReY
 * @subpackage Module.Application
 */
abstract class Controller_base implements \ArrayAccess
{
	//===== Controller control =====//


	/**
	 * Reference to the Application module that instanced this controller
	 * @var Module_Application
	 */
	public static $_app_module;

	/**
	 * Controller's entry point. This is called from the Application module's Flow method
	 * @return null|string If Execute() returns a string, another controller is run ( forwarding ), anything else exists the controller loop.
	 */
	public abstract function Execute(); // all controllers must implement this method

	/**
	 * Integrates another controller run into itself, by capturing the output from that controller chain
	 * @param string $controller Name of the controller
	 * @param array $route New route data for use in the integrated controller
	 */
	public function Integrate($controller, $route=null)
	{
		ob_start();
		self::$_app_module-> controller_Loop($controller, is_null($route)?$this->_route:$route); // reuses same route data or new data
		return ob_get_clean();
	}

	/**
	 * Route data used when calling this controller
	 * @var array
	 */
	protected $_route;

	public function __construct($route)
	{
		$this->_route = $route;
	}

	/**
	 * Commonly used Dynamic Dispatch.
	 * Mostly used for ajax requests, in which one request variable is the "Command" that needs to be executed, however it can also be used to map URI fragments to functions.
	 * Note that the called method does not receive any parameters ! Use class members to send information to that function.
	 *
	 * @param string $command Incoming command
	 * @param string $prefix To prevent accessing unwanted methods, add a prefix to all method names
	 * @return bool|mixed Returns false if the command doesn't exist
	 */
	public function dispatch_Command($command = null, $prefix = '')
	{
		if($command) {
			if(method_exists($this, $prefix.$command))
				return call_user_func(array($this, $prefix.$command));
		}
		return false; // failed to find the function
	}

	//===== Global Application Repository =====//

	/**
	 * Static data available to all loaded controllers, using array access pattern
	 * Also can be used as data "output" from the controllers
	 * @var array
	 */
	protected static $_repository = array();

	/**
	 * (bool)$var = isset($this[$offset]);
	 * @see ArrayAccess::offsetExists()
	 */
	public function offsetExists($offset)
	{
		return isset(self::$_repository[$offset]);
	}

	/**
	 * $var = $this[$offset];
	 * @see ArrayAccess::offsetGet()
	 */
	public function offsetGet($offset)
	{
		if(isset(self::$_repository[$offset]))
			return self::$_repository[$offset];
		else
			return null;
	}

	/**
	 * $this[$offset] = $value;
	 * @see ArrayAccess::offsetSet()
	 */
	public function offsetSet($offset, $value)
	{
		self::$_repository[$offset] = &$value;
	}

	/**
	 * unset($this[$offset]);
	 * @see ArrayAccess::offsetUnset()
	 */
	public function offsetUnset($offset)
	{
		unset(self::$_repository[$offset]);
	}

	//===== Request Handler =====//

	const _REQUEST=0;
	const _GET=1;
	const _POST=2;
	const _COOKIE=3;

	/**
	 * Superglobal to use when reading values ( get / post / request )
	 *
	 * @var string
	 */
	private $_superglobal=self::_REQUEST;

	/**
	 * Retrieves a reference to the request superglobal
	 * TODO: A way to filter unwanted content from the request (for example, to prevent injection - security measure)
	 *
	 * @param string $name
	 * @return mixed
	 */
	public function &__get($name)
	{
		switch($this->_superglobal) {
			case self::_REQUEST:
				return $_REQUEST[$name];
			case self::_GET:
				return $_GET[$name];
			case self::_POST:
				return $_POST[$name];
			case self::_COOKIE:
				return $_COOKIE[$name];
			default:
				$null=null;
				return $null;
		}
	}

	/**
	 * Checks if a superglobal entry exists
	 * @param string $name
	 * @return bool
	 */
	public function __isset($name)
	{
		switch($this->_superglobal) {
			case self::_REQUEST:
				return isset($_REQUEST[$name]);
			case self::_GET:
				return isset($_GET[$name]);
			case self::_POST:
				return isset($_POST[$name]);
			case self::_COOKIE:
				return isset($_COOKIE[$name]);
			default:
				return false;
		}
	}

	/**
	 * Return request array directly
	 * @return array
	 */
	public function &request_toArray()
	{
		switch($this->_superglobal) {
			case self::_REQUEST:
				return $_REQUEST;
			case self::_GET:
				return $_GET;
			case self::_POST:
				return $_POST;
			case self::_COOKIE:
				return $_COOKIE;
			default:
				$empty=array();
				return $empty;
		}
	}

	/**
	 * Sets the type of request data to use when getting values
	 *
	 * @param string $superglobal
	 */
	public function set_Request_Type($superglobal='request')
	{
		switch(strtolower($superglobal)) {
			case 'request':
				$this->_superglobal=self::_REQUEST; break;
			case 'get':
				$this->_superglobal=self::_GET; break;
			case 'post':
				$this->_superglobal=self::_POST; break;
			case 'cookie':
				$this->_superglobal=self::_COOKIE; break;
		}
	}

}
?>